/*
 * Gutter.java
 * Copyright (C) 1999, 2000 mike dillon
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.poxd.syntax;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;

import java.awt.event.InputEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;

import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

import javax.swing.border.Border;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;

public class Gutter extends JComponent implements SwingConstants {
	public Gutter(JEditTextArea textArea, TextAreaDefaults defaults) {
		this.textArea = textArea;

		setBackground(defaults.gutterBgColor);
		setForeground(defaults.gutterFgColor);
		setHighlightedForeground(defaults.gutterHighlightColor);
		setCaretMark(defaults.caretMarkColor);
		setAnchorMark(defaults.anchorMarkColor);
		setSelectionMark(defaults.selectionMarkColor);

		setFont(defaults.gutterFont);
		setBorder(defaults.gutterBorderWidth, defaults.gutterBorderColor);
		setLineNumberAlignment(defaults.gutterNumberAlignment);

		setGutterWidth(defaults.gutterWidth);
		setCollapsed(defaults.gutterCollapsed);

		GutterMouseListener ml = new GutterMouseListener();
		addMouseListener(ml);
		addMouseMotionListener(ml);
	}

	// /////////////////////////////////////////////////////////////////////////////
	// ANTI ALIASING
	// /////////////////////////////////////////////////////////////////////////////

	private boolean antiAliasing = false;
	private boolean wasAntiAliasing = false;

	public void setAntiAliasingEnabled(boolean on) {
		wasAntiAliasing = antiAliasing;
		antiAliasing = on;
	}

	private void setAntiAliasing(Graphics g) {
		// TODO antialiasing management
		// if (antiAliasing)
		// ((Graphics2D)
		// g).setRenderingHints(TextAreaPainter.ANTI_ALIASED_RENDERING);
		// else if (wasAntiAliasing != antiAliasing)
		// ((Graphics2D)
		// g).setRenderingHints(TextAreaPainter.DEFAULT_RENDERING);
	}

	public void paintComponent(Graphics gfx) {
		if (!collapsed) {
			setAntiAliasing(gfx);

			// fill the background
			Rectangle r = gfx.getClipBounds();
			gfx.setColor(getBackground());
			gfx.fillRect(r.x, r.y, r.width, r.height);

			// paint custom highlights, if there are any
			// TODO highlights
			// if (highlights != null)
			// paintCustomHighlights(gfx);

			// paint line numbers, if they are enabled
			if (lineNumberingEnabled)
				paintLineNumbers(gfx);
		}
	}

	protected void paintLineNumbers(Graphics gfx) {
		FontMetrics pfm = textArea.getPainter().getFontMetrics();
		int lineHeight = pfm.getHeight();
		int baseline = (int) Math.round((this.baseline + lineHeight - pfm
				.getMaxDescent()) / 2.0);

		int firstLine = textArea.getFirstLine() + 1;
		int lastLine = firstLine + (getHeight() / lineHeight);

		int firstValidLine = (int) Math.max(1, firstLine);
		int lastValidLine = (int) Math.min(textArea.getLineCount(), lastLine);

		gfx.setFont(getFont());
		gfx.setColor(getForeground());

		String number;

		for (int line = firstLine; line <= lastLine; line++, baseline += lineHeight) {
			// only print numbers for valid lines
			if (line < firstValidLine || line > lastValidLine)
				continue;

			number = Integer.toString(line);
			int offset;

			switch (alignment) {
			case RIGHT:
				offset = gutterSize.width - collapsedSize.width
						- (fm.stringWidth(number) + 3);
				break;
			case CENTER:
				offset = ((gutterSize.width - collapsedSize.width) - fm
						.stringWidth(number)) / 2;
				break;
			case LEFT:
			default:
				offset = 1;
			}

			if (interval > 1 && line % interval == 0) {
				gfx.setColor(getHighlightedForeground());
				gfx.drawString(number, ileft + offset, baseline);
				gfx.setColor(getForeground());
			} else {
				gfx.drawString(number, ileft + offset, baseline);
			}

			if (line == textArea.getCaretLine() + 1) {
				gfx.setColor(caretMark);
				gfx.drawRect(ileft + offset - 8, baseline - 6, 4, 4);
			}

			// TODO anchor management
			// int anchor = ((JextTextArea) textArea).getAnchorOffset();
			// if (anchor != -1 && line == textArea.getLineOfOffset(anchor) + 1)
			// {
			// gfx.setColor(anchorMark);
			// gfx.drawRect(ileft + offset - 8, baseline - 6, 4, 4);
			// }

			if (textArea.getSelectionStart() == textArea.getSelectionEnd()) {
				gfx.setColor(getForeground());
				continue;
			}

			if (line >= textArea.getSelectionStartLine() + 1
					&& line <= textArea.getSelectionEndLine() + 1) {
				gfx.setColor(selectionMark);
				gfx.fillRect(ileft + offset - 7, baseline - 5, 3, 3);
			}

			gfx.setColor(getForeground());
		}
	}

	// protected void paintCustomHighlights(Graphics gfx)
	// {
	// int lineHeight = textArea.getPainter().getFontMetrics() .getHeight();
	// int firstLine = textArea.getFirstLine();
	// int lastLine = firstLine + (getHeight() / lineHeight);
	//
	// int y = 0;
	//
	// for (int line = firstLine; line < lastLine; line++, y += lineHeight)
	// {
	// highlights.paintHighlight(gfx, line, y);
	// }
	// }

	/**
	 * Adds a custom highlight painter.
	 * 
	 * @param highlight
	 *            The highlight
	 */
	// public void addCustomHighlight(TextAreaHighlight highlight)
	// {
	// highlight.init(textArea, highlights);
	// highlights = highlight;
	// }
	/**
	 * Convenience method for setting a default matte border on the right with
	 * the specified border width and color
	 * 
	 * @param width
	 *            The border width (in pixels)
	 * @param color
	 *            The border color
	 */
	public void setBorder(int width, Color color) {
		setBorder(BorderFactory.createMatteBorder(0, 0, 0, width, color));
	}

	/*
	 * JComponent.setBorder(Border) is overridden here to cache the left inset
	 * of the border (if any) to avoid having to fetch it during every repaint.
	 */
	public void setBorder(Border border) {
		super.setBorder(border);

		if (border == null) {
			ileft = 0;
			collapsedSize.width = 0;
			collapsedSize.height = 0;
		} else {
			Insets insets = border.getBorderInsets(this);
			ileft = insets.left;
			collapsedSize.width = insets.left + insets.right;
			collapsedSize.height = insets.top + insets.bottom;
		}
	}

	/*
	 * JComponent.setFont(Font) is overridden here to cache the baseline for the
	 * font. This avoids having to get the font metrics during every repaint.
	 */
	public void setFont(Font font) {
		super.setFont(font);

		fm = getFontMetrics(font);
		baseline = fm.getHeight() - fm.getMaxDescent();
	}

	/**
	 * Set the foreground color for highlighted line numbers
	 * 
	 * @param highlight
	 *            The highlight color
	 */
	public void setHighlightedForeground(Color highlight) {
		intervalHighlight = highlight;
	}

	/**
	 * Get the foreground color for highlighted line numbers
	 * 
	 * @return The highlight color
	 */
	public Color getHighlightedForeground() {
		return intervalHighlight;
	}

	public void setCaretMark(Color mark) {
		caretMark = mark;
	}

	public void setAnchorMark(Color mark) {
		anchorMark = mark;
	}

	public void setSelectionMark(Color mark) {
		selectionMark = mark;
	}

	/**
	 * Set the width of the expanded gutter
	 * 
	 * @param width
	 *            The gutter width
	 */
	public void setGutterWidth(int width) {
		if (width < collapsedSize.width)
			width = collapsedSize.width;
		gutterSize.width = width;
		// if the gutter is expanded, ask the text area to revalidate
		// the layout to resize the gutter
		if (!collapsed)
			textArea.revalidate();
	}

	/**
	 * Get the width of the expanded gutter
	 * 
	 * @return The gutter width
	 */
	public int getGutterWidth() {
		return gutterSize.width;
	}

	/*
	 * Component.getPreferredSize() is overridden here to support the collapsing
	 * behavior.
	 */
	public Dimension getPreferredSize() {
		if (collapsed) {
			return collapsedSize;
		} else {
			return gutterSize;
		}
	}

	public Dimension getMinimumSize() {
		return getPreferredSize();
	}

	// TODO tooltiptext management
	// public String getToolTipText(MouseEvent evt)
	// {
	// return (highlights == null) ? null :
	// highlights.getToolTipText(evt);
	// }

	/**
	 * Identifies whether or not the line numbers are drawn in the gutter
	 * 
	 * @return true if the line numbers are drawn, false otherwise
	 */
	public boolean isLineNumberingEnabled() {
		return lineNumberingEnabled;
	}

	/**
	 * Turns the line numbering on or off and causes the gutter to be repainted.
	 * 
	 * @param enabled
	 *            true if line numbers are drawn, false otherwise
	 */
	public void setLineNumberingEnabled(boolean enabled) {
		if (lineNumberingEnabled == enabled)
			return;

		lineNumberingEnabled = enabled;

		repaint();
	}

	/**
	 * Identifies whether the horizontal alignment of the line numbers.
	 * 
	 * @return Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
	 */
	public int getLineNumberAlignment() {
		return alignment;
	}

	/**
	 * Sets the horizontal alignment of the line numbers.
	 * 
	 * @param alignment
	 *            Gutter.RIGHT, Gutter.CENTER, Gutter.LEFT
	 */
	public void setLineNumberAlignment(int alignment) {
		if (this.alignment == alignment)
			return;

		this.alignment = alignment;

		repaint();
	}

	/**
	 * Identifies whether the gutter is collapsed or expanded.
	 * 
	 * @return true if the gutter is collapsed, false if it is expanded
	 */
	public boolean isCollapsed() {
		return collapsed;
	}

	/**
	 * Sets whether the gutter is collapsed or expanded and force the text area
	 * to update its layout if there is a change.
	 * 
	 * @param collapsed
	 *            true if the gutter is collapsed, false if it is expanded
	 */
	public void setCollapsed(boolean collapsed) {
		if (this.collapsed == collapsed)
			return;

		this.collapsed = collapsed;

		textArea.revalidate();
	}

	/**
	 * Toggles whether the gutter is collapsed or expanded.
	 */
	public void toggleCollapsed() {
		setCollapsed(!collapsed);
	}

	/**
	 * Sets the number of lines between highlighted line numbers.
	 * 
	 * @return The number of lines between highlighted line numbers or zero if
	 *         highlighting is disabled
	 */
	public int getHighlightInterval() {
		return interval;
	}

	/**
	 * Sets the number of lines between highlighted line numbers. Any value less
	 * than or equal to one will result in highlighting being disabled.
	 * 
	 * @param interval
	 *            The number of lines between highlighted line numbers
	 */
	public void setHighlightInterval(int interval) {
		if (interval <= 1)
			interval = 0;
		this.interval = interval;
		repaint();
	}

	public JPopupMenu getContextMenu() {
		return context;
	}

	public void setContextMenu(JPopupMenu context) {
		this.context = context;
	}

	// private members
	// the JEditTextArea this gutter is attached to
	private JEditTextArea textArea;

	private JPopupMenu context;

	// private TextAreaHighlight highlights;

	private int baseline = 0;
	private int ileft = 0;

	private Dimension gutterSize = new Dimension(0, 0);
	private Dimension collapsedSize = new Dimension(0, 0);

	private Color intervalHighlight;
	private Color caretMark;
	private Color anchorMark;
	private Color selectionMark;

	private FontMetrics fm;

	private int alignment;

	private int interval = 0;
	private boolean lineNumberingEnabled = true;
	private boolean collapsed = false;

	class GutterMouseListener extends MouseAdapter implements
			MouseMotionListener {
		public void mouseClicked(MouseEvent e) {
			int count = e.getClickCount();
			if (count == 1) {
				if (context == null || context.isVisible())
					return;

				if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
					// XXX this is a hack to make sure the
					// XXX actions get the right text area
					textArea.requestFocus();
					context.show(Gutter.this, e.getX(), e.getY());
				}
			} else if (count >= 2) {
				toggleCollapsed();
			}
		}

		public void mousePressed(MouseEvent e) {
			dragStart = e.getPoint();
			startWidth = gutterSize.width;
		}

		public void mouseDragged(MouseEvent e) {
			if (dragStart == null)
				return;

			if (isCollapsed())
				setCollapsed(false);

			Point p = e.getPoint();
			gutterSize.width = startWidth + p.x - dragStart.x;

			if (gutterSize.width < collapsedSize.width) {
				gutterSize.width = startWidth;
				setCollapsed(true);
			}

			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					textArea.revalidate();
				}
			});
		}

		public void mouseExited(MouseEvent e) {
			if (dragStart != null && dragStart.x > e.getPoint().x) {
				setCollapsed(true);
				gutterSize.width = startWidth;

				SwingUtilities.invokeLater(new Runnable() {
					public void run() {
						textArea.revalidate();
					}
				});
			}

			// dragStart = null;
		}

		public void mouseMoved(MouseEvent e) {
		}

		public void mouseReleased(MouseEvent e) {
			dragStart = null;
		}

		private Point dragStart = null;
		private int startWidth = 0;
	}
}